home *** CD-ROM | disk | FTP | other *** search
/ Chip 2006 June / CHIP 2006-06.2.iso / program / freeware / Democracy-0.8.2.exe / xulrunner / python / guide.py < prev    next >
Encoding:
Python Source  |  2006-04-10  |  7.6 KB  |  196 lines

  1. from database import DDBObject
  2. from downloader import grabURL
  3. from scheduler import ScheduleEvent
  4. from xhtmltools import urlencode
  5. from copy import copy
  6. import re
  7. import config
  8. import threading
  9. import urllib
  10.  
  11. HTMLPattern = re.compile("^.*(<head.*?>.*</body\s*>)", re.S)
  12.  
  13. # NEEDS: Make this something more attractive
  14. guideNotAvailableBody = """
  15. <body>
  16.   <script type=\"text/javascript\">
  17.     function tryAgain() {
  18.       eventURL('template:guide-loading');
  19.     }
  20.   </script>
  21.  
  22.   <p>
  23.     The channel guide could not be loaded. Perhaps you're not connected to the
  24.     Internet?
  25.   </p>
  26.   <p>
  27.     <a href="#" onclick="tryAgain();">Try again</a>
  28.   </p>
  29. </body>
  30. """
  31.  
  32. # Desired semantics:
  33. #  * The first time getHTML() is called (ever, across sessions), the user
  34. #    gets 'firstTimeIntroBody' above. Subsequent times, she gets the <body>
  35. #    part of the document returned from CHANNEL_GUIDE_URL (the 'network guide
  36. #    body'.)
  37. #  * The network guide body is retrieved at program startup, and once an
  38. #    hour after that, so that getHTML() can return immediately. If retrieval
  39. #    fails, we just skip that hourly update. The last-retrieved copy is
  40. #    kept in memory, and it is saved to disk across executions of the program.
  41. #  * When getHTML() is called, we just return that cached copy. That might
  42. #    be from the previous run of the program if we haven't succeeded in a
  43. #    retrieval during this session.
  44. #  * If we have *never* succeeded in retrieving the channel guide, we put up
  45. #    an error page and immediately schedule a reload. We provide a "try again"
  46. #    link that simply reloads the guide. Hopefully by the time the user
  47. #    manages to click it, the scheduled update attempt will have completed.
  48. #
  49. # NEEDS: Right now, there's a race between execution (and completion)
  50. # of the first update and the first call to getHTML(). On Windows, the
  51. # latter has a tendency to happen first, meaning that on our first
  52. # view of the channel guide in a session we see a stale copy of the
  53. # guide from the last run. This usually isn't a problem on the first
  54. # run, because the load will finish while the user is messing around
  55. # with the tutorial.
  56. #
  57. # Fixing this is tricky -- do we want to block waiting for the update
  58. # to complete during that first load? Then we need to redesign some of
  59. # the frontend semantics somewhere, because this causes nasty
  60. # lockup-like behavior on Windows. (It might be sufficient to call
  61. # onStartup from a new thread rather than from a JS window creation
  62. # event handler calling into Python.)
  63.  
  64. class ChannelGuide(DDBObject):
  65.     def __init__(self):
  66.         # True if user has seen the tutorial
  67.         self.sawIntro = False 
  68.         # If None, we have never successfully loaded the guide. Otherwise,
  69.         # the <body> part of the front channel guide page from the last time
  70.         # we loaded it, whenever that was (perhaps in a previous session.)
  71.         self.cachedGuideBody = None
  72.         # True if we have successfully loaded the channel guide in this
  73.         # session.
  74.         self.loadedThisSession = False
  75.         # Condition variable protecting access to above; signalled when
  76.         # loadedThisSession changes.
  77.         self.cond = threading.Condition()
  78.         DDBObject.__init__(self)
  79.         # Start loading the channel guide.
  80.         self.startLoadsIfNecessary()
  81.  
  82.     ##
  83.     # Called by pickle during serialization
  84.     def __getstate__(self):
  85.         temp = copy(self.__dict__)
  86.         del temp['cond']
  87.         del temp['loadedThisSession']
  88.         return (1,temp)
  89.  
  90.     ##
  91.     # Called by pickle during deserialization
  92.     def __setstate__(self,state):
  93.         (version, data) = state
  94.  
  95.         if version == 0:
  96.             self.sawIntro = data['viewed']
  97.             self.cachedGuideBody = None
  98.             self.loadedThisSession = False
  99.             self.cond = threading.Condition()
  100.         else:
  101.             assert(version == 1)
  102.             self.__dict__ = data
  103.             self.cond = threading.Condition()
  104.             self.loadedThisSession = False
  105.  
  106.         # Try to get a fresh version.
  107.         # NEEDS: There's a race between self.update finishing and
  108.         # getHTML() being called. If the latter happens first, we might get
  109.         # the version of the channel guide from the last time DTV was run even
  110.         # if we have a perfectly good net connection.
  111.         self.startLoadsIfNecessary()
  112.  
  113.     def startLoadsIfNecessary(self):
  114.         import frontend
  115.         if frontend.getDTVAPIURL():
  116.             # Uses direct browsing. No precaching needed here.
  117.             pass
  118.         else:
  119.             # Uses precaching. Set up an initial update, plus hourly reloads..
  120.             ScheduleEvent(0, self.update, False)
  121.             ScheduleEvent(3600, self.update, True)
  122.  
  123.     def setSawIntro(self):
  124.         self.sawIntro = True
  125.  
  126.     # How should we load the guide? Returns (scheme, value). If scheme is
  127.     # 'url', value is a URL that should be loaded directly in the frame.
  128.     # If scheme is 'template', value is the template that should be loaded in
  129.     # the frame.
  130.     def getLocation(self):
  131.         if not self.sawIntro:
  132.             return ('template', 'first-time-intro')
  133.  
  134.         import frontend
  135.         apiurl = frontend.getDTVAPIURL()
  136.         if apiurl:
  137.             # We're on a platform that uses direct loads and DTVAPI.
  138.             url = config.get(config.CHANNEL_GUIDE_URL)
  139.             apiurl = urllib.quote_plus(apiurl)
  140.             apicookie = urllib.quote_plus(frontend.getDTVAPICookie())
  141.             url = "%s?dtvapiURL=%s&dtvapiCookie=%s" % (url, apiurl, apicookie)
  142.             return ('url', url)
  143.  
  144.         # We're on a platform that uses template inclusions and URL
  145.         # interception.
  146.         return ('template', 'guide')
  147.  
  148.     def getHTML(self):
  149.         self.cond.acquire()
  150.         try:
  151.             # In the future, may want to use
  152.             # self.loadedThisSession to tell if this is a fresh
  153.             # copy of the channel guide, and/or block a bit to
  154.             # give the initial load a chance to succeed or fail
  155.             # (but this would require changing the frontend code
  156.             # to expect the template code to block, and in general
  157.             # seems like a bad idea.)
  158.             #
  159.             # A better solution would be to put up a "loading" page and
  160.             # somehow shove an event to the page when the channel guide
  161.             # load finishes that causes the browser to reload the page.
  162.             if not self.cachedGuideBody:
  163.                 # Start a new attempt, so that clicking on the guide
  164.                 # tab again has at least a chance of working
  165.                 print "DTV: No guide available! Sending apology instead."
  166.                 ScheduleEvent(0, self.update, False)
  167.                 return guideNotAvailableBody
  168.             else:
  169.                 if not self.loadedThisSession:
  170.                     print "DTV: *** WARNING *** loading a stale copy of the chanel guide from cache"
  171.                 return self.cachedGuideBody
  172.         finally:
  173.             self.cond.release()
  174.  
  175.     def update(self):
  176.         # We grab the URL and convert the HTML to JavaScript so it can
  177.         # be loaded from a plain old template. It's less elegant than
  178.         # making another kind of feed object, but it makes it easier
  179.         # for non-programmers to work with
  180.         print "DTV: updating the Guide"
  181.         url = config.get(config.CHANNEL_GUIDE_URL)
  182.  
  183.         info = grabURL(url)
  184.         if info is not None:
  185.             html = info['file-handle'].read()
  186.             info['file-handle'].close()
  187.  
  188.             # Put the HTML into the cache
  189.             self.cond.acquire()
  190.             try:
  191.                 self.cachedGuideBody = HTMLPattern.match(html).group(1)
  192.                 self.loadedThisSession = True
  193.                 self.cond.notify()
  194.             finally:
  195.                 self.cond.release()
  196.